View Javadoc
1 /* 2 * Scope: a generic MVC framework. 3 * Copyright (c) 2000-2002, The Scope team 4 * All rights reserved. 5 * 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 14 * Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 18 * Neither the name "Scope" nor the names of its contributors 19 * may be used to endorse or promote products derived from this software 20 * without specific prior written permission. 21 * 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 28 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 * 35 * 36 * $Id: ScopeServlet.java,v 1.16 2002/09/06 16:11:48 ludovicc Exp $ 37 */ 38 package org.scopemvc.controller.servlet; 39 40 41 import java.io.IOException; 42 import java.util.Enumeration; 43 import java.util.HashMap; 44 import java.util.Iterator; 45 import java.util.List; 46 import javax.servlet.ServletException; 47 import javax.servlet.http.HttpServlet; 48 import javax.servlet.http.HttpServletRequest; 49 import javax.servlet.http.HttpServletResponse; 50 import org.apache.commons.logging.Log; 51 import org.apache.commons.logging.LogFactory; 52 import org.scopemvc.core.Control; 53 import org.scopemvc.core.Controller; 54 import org.scopemvc.util.BasicObjectPool; 55 import org.scopemvc.util.Debug; 56 import org.scopemvc.util.ObjectPool; 57 import org.scopemvc.util.PoolableObjectFactory; 58 import org.scopemvc.util.ScopeConfig; 59 import org.scopemvc.view.servlet.Page; 60 import org.scopemvc.view.servlet.ServletView; 61 import org.scopemvc.controller.basic.BasicController; 62 import org.scopemvc.controller.basic.ViewContext; 63 64 /*** 65 * <P> 66 * 67 * Base class for a web app's servlet dispatcher: subclass this to implement 68 * application startup and initialisation (use a static initializer). This class 69 * accepts incoming requests, collects the parameters into a mutable HashMap, 70 * parses them to create a Control and to find a ViewID to identify the View the 71 * user interacted with. The request parameters are then passed to the View to 72 * populate its Model, before the View issues the Control for the owning 73 * Controller to handle. </P> <P> 74 * 75 * A configurable number of Application Controllers (and sub-Controllers and 76 * their Views and Models) are created on the first request. These are put into 77 * a pool to be shared between all future requests. For this reason, Controllers 78 * that are shared must be aware of the possible need to reset their model's 79 * state before handling a Control. </P> <P> 80 * 81 * A form request is handled as follows:<BR /> 82 * <A HREF="../../../../../images/api/ScopeServlet.doPost.gif"> <IMG 83 * SRC="../../../../../images/api/ScopeServlet.doPost.gif" WIDTH="240" 84 * HEIGHT="240"> </A> </P> <P> 85 * 86 * Most steps in this sequence are implemented by Template Methods that can be 87 * overridden to change the default behaviour. </P> <P> 88 * 89 * The issue of session state management and model scope is not resolved here. 90 * </P> <P> 91 * 92 * See the various XML/XSLT and JSP servlet samples for examples of use. </P> 93 * 94 * @author <A HREF="mailto:smeyfroi@users.sourceforge.net">Steve Meyfroidt</A> 95 * @created 05 August 2002 96 * @version $Revision: 1.16 $ $Date: 2002/09/06 16:11:48 $ 97 * @see ServletContext 98 * @see org.scopemvc.controller.servlet.xml.XSLScopeServlet 99 * @see org.scopemvc.controller.servlet.jsp.JSPScopeServlet 100 */ 101 public abstract class ScopeServlet extends HttpServlet { 102 103 /*** 104 * The default validation error handler in {@link #handleValidationFailures} 105 * puts the list of {@link org.scopemvc.view.servlet.ValidationFailure}s in 106 * the properties of the {@link org.scopemvc.controller.basic.ViewContext} 107 * under this key for later retrieval by a Controller. 108 */ 109 public final static String VALIDATION_FAILURES = "org.scopemvc.controller.servlet.ValidationFailures"; 110 111 /*** 112 * The key used to identify the Control ID in the request parameters for 113 * this implementation. Initialised from the 114 * "org.scopemvc.controller.servlet.ScopeServlet.ControlParam" value from 115 * ScopeConfig. 116 * 117 * @see #createControl 118 */ 119 public static String CONTROL_PARAM; 120 121 /*** 122 * The request's key that identifies the View ID used to find the active 123 * View that a Control was sent from. Initialised from the 124 * "org.scopemvc.controller.servlet.ScopeServlet.ViewIDParam" value of 125 * ScopeConfig. 126 * 127 * @see #findPageByID 128 */ 129 public static String VIEW_ID_PARAM; 130 131 private final static Log LOG = LogFactory.getLog(ScopeServlet.class); 132 133 /*** 134 * Pool of shared application Controllers for this servlet instance. 135 */ 136 protected ObjectPool sharedControllerPool = null; 137 138 139 /*** 140 * Constructor for the ScopeServlet object 141 */ 142 public ScopeServlet() { 143 // Initialise statics from config. Do it this way rather than 144 // in static initializers to allow custom ScopeConfigs to be 145 // installed and used. 146 VIEW_ID_PARAM = ScopeConfig.getString("org.scopemvc.controller.servlet.ScopeServlet.ViewIDParam"); 147 if (VIEW_ID_PARAM == null) { 148 LOG.fatal("No ViewIDParam in config."); 149 } 150 151 CONTROL_PARAM = ScopeConfig.getString("org.scopemvc.controller.servlet.ScopeServlet.ControlParam"); 152 if (CONTROL_PARAM == null) { 153 LOG.fatal("No ControlParam in config."); 154 } 155 156 // Create a pool of shared applications to handle requests 157 try { 158 initSharedControllerPool(); 159 } catch (Exception e) { 160 LOG.fatal("Can't create shared pool of applications", e); 161 throw new RuntimeException("Can't create application!\n" + e.toString()); 162 } 163 } 164 165 // StringBuffer message = new StringBuffer(); 166 // for (Iterator i = inFailures.iterator(); i.hasNext(); ) { 167 // Object o = i.next(); 168 // if (Debug.ON) Debug.assert(o instanceof ValidationFailure); 169 // ValidationFailure failure = (ValidationFailure)o; 170 // 171 // message.append("Failed to set "); 172 // message.append(failure.getProperty()); 173 // message.append(" to "); 174 // message.append(failure.getValue()); 175 // message.append(" because <I>"); 176 // message.append(failure.getException().getLocalizedMessage()); 177 // message.append("</I><BR/>"); 178 // } 179 // 180 // ViewContext.getViewContext().showError("Validation failed", message.toString()); 181 // return true; // the request has been completed 182 // } 183 184 185 /*** 186 * Call from a Controller instead of showing a View to force an internal 187 * redirect. Pass a HashMap of form parameters for the new request. 188 * 189 * @param inFormParameters TODO: Describe the Parameter 190 */ 191 public static void redirect(HashMap inFormParameters) { 192 if (Debug.ON) { 193 Debug.assertTrue(ViewContext.getViewContext() instanceof ServletContext); 194 } 195 ServletContext context = (ServletContext) ViewContext.getViewContext(); 196 197 context.getServlet().handleRequest(inFormParameters); 198 } 199 200 201 /*** 202 * Copy references to all form parameters into a mutable Map. If a parameter 203 * has multiple values then the parameter value will be a String[] else a 204 * String. <P> 205 * 206 * This is a useful place to insert default values for missing parameters, 207 * for example to map .../servlet/MyServlet onto some default "action" and 208 * "view" by inserting those default parameters into the returned HashMap if 209 * missing in incoming request. </P> 210 * 211 * @param inRequest find parameters in this request 212 * @return Map containing references to all form parameters, either String 213 * or String[] if multiple values. 214 */ 215 protected HashMap getFormParameters(HttpServletRequest inRequest) { 216 HashMap result = new HashMap(); 217 for (Enumeration e = inRequest.getParameterNames(); e.hasMoreElements(); ) { 218 219 Object o = e.nextElement(); 220 if (Debug.ON) { 221 Debug.assertTrue(o instanceof String); 222 } 223 String name = (String) o; 224 225 o = inRequest.getParameterValues(name); 226 if (Debug.ON) { 227 Debug.assertTrue(o instanceof String[], "not String[]: " + o); 228 } 229 String[] values = (String[]) o; 230 if (Debug.ON) { 231 Debug.assertTrue(values.length >= 1); 232 } 233 234 if (values.length == 1) { 235 result.put(name, values[0]); 236 } else { 237 result.put(name, values); 238 } 239 } 240 return result; 241 } 242 243 244 /*** 245 * This implementation maps GET requests onto POST requests. 246 * 247 * @param req TODO: Describe the Parameter 248 * @param resp TODO: Describe the Parameter 249 * @throws ServletException TODO: Describe the Exception 250 * @throws IOException TODO: Describe the Exception 251 */ 252 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 253 throws ServletException, IOException { 254 if (LOG.isDebugEnabled()) { 255 LOG.debug("doGet: " + req + ", " + resp); 256 } 257 doPost(req, resp); 258 } 259 260 261 /*** 262 * Default implementation uses shared application instances. 263 * 264 * @param req TODO: Describe the Parameter 265 * @param resp TODO: Describe the Parameter 266 * @throws ServletException TODO: Describe the Exception 267 * @throws IOException TODO: Describe the Exception 268 */ 269 protected void doPost(HttpServletRequest req, HttpServletResponse resp) 270 throws ServletException, IOException { 271 // Useful debug output 272 if (LOG.isDebugEnabled()) { 273 LOG.debug("doPost: " + req + ", " + resp); 274 } 275 if (LOG.isDebugEnabled()) { 276 for (Enumeration e = req.getParameterNames(); e.hasMoreElements(); ) { 277 Object o = e.nextElement(); 278 LOG.debug("doPost: (" + o + ")=(" + req.getParameter(o.toString()) + ")"); 279 } 280 } 281 282 // Copy references to the form parameters into a mutable container 283 HashMap formParameters = getFormParameters(req); 284 285 // Install a ViewContext for this request if one not already set (by subclass override) 286 if (Debug.ON) { 287 Debug.assertTrue(ViewContext.getViewContext() == null || ViewContext.getViewContext() instanceof ServletContext); 288 } 289 if (ViewContext.getViewContext() == null) { 290 ViewContext.setThreadContext(createServletContext(req, resp, formParameters)); 291 } 292 293 // Need a 'finally' to clear the ViewContext after handling the request 294 try { 295 296 // Now handle the request using the parameters 297 handleRequest(formParameters); 298 299 } catch (Exception e) { 300 LOG.fatal("doPost failed", e); 301 throw new ServletException(e); 302 } finally { 303 // Clear the ViewContext for this request (Thread) 304 ViewContext.clearThreadContext(); 305 } 306 } 307 308 309 /*** 310 * @param formParameters TODO: Describe the Parameter 311 * @todo document the method 312 */ 313 protected void handleRequest(HashMap formParameters) { 314 315 // Make a Control from the form parameters 316 Control control = createControl(formParameters); 317 318 // Find the View ID from the form parameters 319 String viewID = findViewID(formParameters); 320 321 // Get an application from the shared pool to handle the request 322 if (Debug.ON) { 323 Debug.assertTrue(sharedControllerPool != null, "no controller pool"); 324 } 325 Controller applicationController = (Controller) sharedControllerPool.borrowObject(); 326 if (Debug.ON) { 327 Debug.assertTrue(applicationController != null, "can't find an applicationController"); 328 } 329 330 // Need a finally block to return the shared application to the pool 331 try { 332 333 // Find the Page the user interacted with by the incoming View ID 334 Page page = findPageByID(applicationController, viewID); 335 // If no Page found then try to get a default Page ***** don't like this behaviour... option to invoke an error handler? 336 if (page == null) { 337 page = findDefaultPage(applicationController); 338 } 339 340 if (page != null) { 341 342 // Let the Page populate its bound model if it wants to 343 List failures = page.populateModel(formParameters); 344 if (failures != null) { 345 // Validation errors during the populate are handled here. 346 // On return true the error was completely handled so don't carry on. 347 if (handleValidationFailures(page, failures)) { 348 return; 349 } 350 } 351 352 // Now issue the Control, or call BasicController.startup() if no Control. 353 // Note that ControlExceptions are handled by the normal ViewContext.showError() mechanism. 354 if (control == null) { 355 BasicController controller = (BasicController) page.getController(); 356 controller.startup(); 357 } else { 358 page.issueControl(control); 359 } 360 } 361 362 // If no View shown so far then force a showView on the active Controller's ServletView 363 if (Debug.ON) { 364 Debug.assertTrue(ViewContext.getViewContext() instanceof ServletContext); 365 } 366 ServletContext context = (ServletContext) ViewContext.getViewContext(); 367 if (!context.hasShownView()) { 368 if (LOG.isDebugEnabled()) { 369 LOG.debug("doPost: not shown view so showing: " + page); 370 } 371 ServletView sv = page.getParent(); 372 context.showView(sv); 373 } 374 } finally { 375 sharedControllerPool.returnObject(applicationController); 376 } 377 } 378 379 380 /*** 381 * Create a ViewContext that will be used for a request: default impl here 382 * returns a new {@link ServletContext}. For example to implement your own 383 * error handling, extend the default ServletContext to override showError, 384 * and then override this method in your servlet subclass to create an 385 * instance of your own ServletContext. 386 * 387 * @param req TODO: Describe the Parameter 388 * @param resp TODO: Describe the Parameter 389 * @param inFormParameters TODO: Describe the Parameter 390 * @return TODO: Describe the Return Value 391 */ 392 protected abstract ServletContext createServletContext(HttpServletRequest req, HttpServletResponse resp, HashMap inFormParameters); 393 394 395 /*** 396 * <P> 397 * 398 * Return a Control instance from request form parameters getting the 399 * Control ID from the CONTROL_PARAM form value, and setting the 400 * formParameters HashMap as the Control's parameter. Also handles imagemap 401 * requests of the form <CODE>imagename.x=ControlId</CODE>. </P> <P> 402 * 403 * Override this for an application to create a default Control when none is 404 * supplied in the request, eg for a simple home request: <PRE>http://localhost:8080/myapp/MyServlet<;/PRE> 405 * with no parameters to display the application's home page. But also see 406 * {@link #getFormParameters}. </P> 407 * 408 * @param ioFormParameters request's form parameters. 409 * @return Control instance created from the form parameters 410 */ 411 protected Control createControl(HashMap ioFormParameters) { 412 413 Object o = ioFormParameters.get(CONTROL_PARAM); 414 if (o instanceof String[]) { 415 LOG.warn("Multiple Control parameters (using first one): " + o); 416 if (Debug.ON) { 417 Debug.assertTrue(((String[]) o).length > 0); 418 } 419 o = ((String[]) o)[0]; 420 } 421 422 if (Debug.ON) { 423 Debug.assertTrue(o == null || o instanceof String); 424 } 425 String controlID = (String) o; 426 if (controlID != null && controlID.length() < 1) { 427 controlID = null; 428 } 429 430 // Got it? 431 if (controlID != null) { 432 return new Control(controlID, ioFormParameters); 433 } 434 435 // Search all params looking for something of the form "name.x" coming from an image button 436 for (Iterator i = ioFormParameters.keySet().iterator(); i.hasNext(); ) { 437 o = i.next(); 438 if (o instanceof String) { 439 String name = (String) o; 440 if (name.endsWith(".x")) { 441 controlID = name.substring(0, name.length() - 2); 442 break; 443 } 444 } 445 } 446 447 // Got a Control ID? Then make a Control and remove the foo.x and foo.y parameters 448 if (controlID != null && controlID.length() > 0) { 449 return new Control(controlID, ioFormParameters); 450 } 451 452 // Got no control 453 return null; 454 } 455 456 457 /*** 458 * Could be overidden to provide a default ViewID if none in the parameters, 459 * but also see {@link #getFormParameters}. 460 * 461 * @param inParameters TODO: Describe the Parameter 462 * @return TODO: Describe the Return Value 463 */ 464 protected String findViewID(HashMap inParameters) { 465 return (String) inParameters.get(VIEW_ID_PARAM); 466 } 467 468 469 /*** 470 * Search through application's Controller hierarchy to find a Page matching 471 * the ViewID. Return null if not found. 472 * 473 * @param inRootController TODO: Describe the Parameter 474 * @param inViewID TODO: Describe the Parameter 475 * @return TODO: Describe the Return Value 476 */ 477 protected Page findPageByID(Controller inRootController, String inViewID) { 478 if (LOG.isDebugEnabled()) { 479 LOG.debug("findPageByID: " + inRootController + ", " + inViewID); 480 } 481 if (Debug.ON) { 482 Debug.assertTrue(inRootController != null); 483 } 484 if (!(inRootController instanceof BasicController)) { 485 throw new RuntimeException("ScopeServlet relies on application Controllers being instanceof BasicController."); 486 } 487 488 // Search at the current root 489 if (Debug.ON) { 490 Debug.assertTrue(inRootController.getView() == null || inRootController.getView() instanceof ServletView); 491 } 492 ServletView v = (ServletView) inRootController.getView(); 493 if (v != null) { 494 Page p = v.findPageByID(inViewID); 495 if (p != null) { 496 return p; 497 } 498 } 499 500 // If not there, recurse through all children 501 for (Iterator i = ((BasicController) inRootController).getChildren().iterator(); i.hasNext(); ) { 502 Object o = i.next(); 503 if (Debug.ON) { 504 Debug.assertTrue(o instanceof Controller); 505 } 506 Page result = findPageByID((Controller) o, inViewID); 507 if (result != null) { 508 return result; 509 } 510 } 511 512 // And if not found then return null 513 return null; 514 } 515 516 517 /*** 518 * If the request ViewID doesn't match any Page then this provides a default 519 * Page: could be the start page of the application that the user gets to by 520 * invoking the servlet with no parameters. Here returns the first Page 521 * found by a depth-first traversal of the application hierarchy. <P> 522 * 523 * If you manage the {@link #getFormParameters} method to validate form 524 * parameters then this method might never be used. But if there's a single 525 * page that you want to use when an invalid ViewID is passed (eg an error 526 * page) then this is the place to do it. </P> <P> 527 * 528 * Don't like this. Should be allowed to redirect to an error handler on 529 * invalid ViewID? ***** </P> 530 * 531 * @param inRootController TODO: Describe the Parameter 532 * @return TODO: Describe the Return Value 533 */ 534 protected Page findDefaultPage(Controller inRootController) { 535 // Search at the current root 536 if (Debug.ON) { 537 Debug.assertTrue(inRootController.getView() == null || inRootController.getView() instanceof ServletView); 538 } 539 ServletView v = (ServletView) inRootController.getView(); 540 if (v != null) { 541 Page result = v.getFirstPage(); 542 if (result != null) { 543 return result; 544 } 545 } 546 547 for (Iterator i = ((BasicController) inRootController).getChildren().iterator(); i.hasNext(); ) { 548 Object o = i.next(); 549 if (Debug.ON) { 550 Debug.assertTrue(o instanceof Controller); 551 } 552 Page result = findDefaultPage((Controller) o); 553 if (result != null) { 554 return result; 555 } 556 } 557 558 // And if not found then return null 559 return null; 560 } 561 562 563 /*** 564 * <P> 565 * 566 * Called if an exception is thrown by {@link 567 * org.scopemvc.view.servlet.Page#populateModel Page.populateModel}. </P> 568 * <P> 569 * 570 * Default implementation here puts the List of {@link 571 * org.scopemvc.view.servlet.ValidationFailure}s into the ViewContext under 572 * the {@link #VALIDATION_FAILURES} key for retrieval ({@link 573 * org.scopemvc.controller.basic.ViewContext#getProperty} and handling by 574 * Controllers, ie: <PRE> 575 * protected void doSomeHandler() throws ControlException { 576 * List validationFailures = (List)ViewContext.getViewContext().getProperty(ScopeServlet.VALIDATION_FAILURES); 577 * if (validationFailures != null) { 578 * // TODO: Handle the validation failures 579 * } else { 580 * // TODO: No validation failures so handle the control 581 * } 582 * } 583 * </PRE> </P> 584 * 585 * @param inPage TODO: Describe the Parameter 586 * @param inFailures TODO: Describe the Parameter 587 * @return true if this handler has finished the request, ie the normal 588 * request handler can stop immediately. 589 */ 590 protected boolean handleValidationFailures(Page inPage, List inFailures) { 591 if (LOG.isDebugEnabled()) { 592 LOG.debug("handleValidationFailures: " + inFailures); 593 } 594 if (Debug.ON) { 595 Debug.assertTrue(inFailures != null); 596 } 597 598 ViewContext.getViewContext().addProperty(VALIDATION_FAILURES, inFailures); 599 return false; 600 } 601 602 603 /*** 604 * <P> 605 * 606 * Override this to create the root application Controller. The application 607 * Controller should setup any child Controllers that it needs to handle 608 * parts of the application for it. </P> 609 * 610 * @return new application Controller 611 * @exception Exception on any failure 612 */ 613 protected abstract Controller createApplicationController() throws Exception; 614 615 616 // --------------- Shared controller pool ------------------------- 617 618 /*** 619 * @todo document the method 620 * @throws Exception TODO: Describe the Exception 621 */ 622 protected void initSharedControllerPool() throws Exception { 623 int size = 10; 624 Integer i = ScopeConfig.getInteger("org.scopemvc.controller.servlet.ScopeServlet.maxControllerPoolSize"); 625 if (i != null) { 626 size = i.intValue(); 627 } 628 sharedControllerPool = new BasicObjectPool(new SharedControllerFactory(), size); 629 } 630 631 632 /*** 633 * @author smefroi 634 * @created 05 August 2002 635 * @todo document the class 636 */ 637 protected class SharedControllerFactory implements PoolableObjectFactory { 638 /*** 639 * @return TODO: Describe the Return Value 640 * @todo document the method 641 */ 642 public Object createObject() { 643 try { 644 return createApplicationController(); 645 } catch (Exception e) { 646 LOG.fatal("Can't create application Controller", e); 647 return null; 648 } 649 } 650 651 /*** 652 * @param object TODO: Describe the Parameter 653 * @todo document the method 654 */ 655 public void destroyObject(Object object) { 656 // noop 657 } 658 659 /*** 660 * @param object TODO: Describe the Parameter 661 * @todo document the method 662 */ 663 public void activateObject(Object object) { 664 // noop 665 } 666 667 /*** 668 * @param object TODO: Describe the Parameter 669 * @todo document the method 670 */ 671 public void passivateObject(Object object) { 672 // noop 673 } 674 } 675 }

This page was automatically generated by Maven